if (typeof JRDL == "undefined" || !JRDL) {
    var JRDL = {};
}

(function () {

var ns = JRDL;


// The ExtJS library is currently required, but could be removed by
// providing alternate versions of the following functions and objects.

var format = String.format;
var namespace = Ext.namespace;
var extend = Ext.extend;
var merge = Ext.apply;
var forEach = Ext.each;
var Observable = Ext.util.Observable;
var Connection = Ext.data.Connection;

var jsonEncode = function () {
    return Ext.encode.apply(Ext, arguments);
};

var jsonDecode = function () {
    return Ext.decode.apply(Ext, arguments);
};


var empty = function (d) {
    return !!(d === null || d === undefined);
};

var queue = function (fn, scope) {
    if (!fn) {
        return;
    }
    
    setTimeout(function () {
        fn.call(scope);
    }, 0);
};


var logger = function (msg) {
    var args = Array.prototype.slice.apply(arguments);
    if (args.length > 1) {
        console.log(format.apply(null, args));
    } else {
        console.log(msg);
    }
};
var emptyLogger = function () {};
var log = emptyLogger;
ns.log = log;
ns.enableLog = function () {
    log = logger;
};

ns.disableLog = function () {
    log = emptyLogger;
};

ns.extend = extend;
ns.namespace = namespace;


ns.Service = extend(Observable, {
    constructor: function (path) {
        this.path = path;
        
        this.suspendOnUnauthorized = true;
        this.suspended = false;
        this.requestQueue = [];
        this.requestHeaders = {};
        
        this.connection = new Connection();
        
        this.addEvents('unauthorized');
        
        this.String = ns.String;
        this.Number = ns.Number;
        this.Boolean = ns.Boolean;
    },
    
    // Accepts ('header', 'value') or 
    // ({header1: 'value', 'header2': value})
    setRequestHeader: function (header, value) {
        if (typeof header === "object") {
            for (var h in header) {
                this.setRequestHeader(h, header[h]);
            }
            return;
        }
        log('Setting request header "{0}" to "{1}"', header, value);
        this.requestHeaders[header] = value;
    },
    
    removeRequestHeader: function (/*header1, header2, ...*/) {
        for (var i = 0; i < arguments.length; i++) {
            log('Removing request header "{0}"', arguments[i]);
            delete this.requestHeaders[arguments[i]];
        }
    },
    
    clearRequestHeaders: function () {
        log('Clearing request headers');
        this.requestHeaders = {};
    },
    
    addRequestExceptionHandler: function (handler, scope) {
        log('Added request exception handler');
        this.connection.on('requestexception', handler, scope);
    },
    
    addRequestCompleteHandler: function (handler, scope) {
        log('Added request completion handler');
        this.connection.on('requestcomplete', handler, scope);
    },
    
    sendRequest: function (req) {
        if (this.suspended) {
            this.queueRequest(req);
            return;
        }
        
        log('Sending {0} request to "{1}"', req.method, req.url);
        
        this.connection.request({
            url: req.url,
            method: req.method,
            headers: this.requestHeaders,
            jsonData: (req.data) ? jsonEncode(req.data) : null,
            cb: function (options, success, response) {
                var data = null;
                if (success) {
                    log("Successful XHR for '{0}'.", req.url);
                    try {
                        log("Decoding data for '{0}'.", req.url);
                        data = jsonDecode(response.responseText);
                    } catch (e) {
                        log("Error decoding data for '{0}'.", req.url);
                        log(e.toString());
                    }
                } else {
                    log("Unsccessful XHR for '{0}'.", req.url);
                    // What was the reason?
                    if (response.status === -1) {
                        // Time out
                        // What should we do now? Try again?
                    } else if (response.status === 401) {
                        // Unauthorized!
                        // Let's notify and suspend all requests
                        // This request should be re-tried once 
                        // requests are resumed.
                        this.suspendRequests();
                        this.queueRequest(req);
                        this.fireEvent('unauthorized');
                        return;
                    } else {
                        // Some other error.
                        // I don't think we can recover.
                    }
                }
                
                queue(function () {
                    if (req.cb) {
                        req.cb.call(req.scope, data, response);
                    }
                });
            }, 
            scope: this
        });
    },
    
    get: function (url, cb, scope) {
        this.sendRequest({
            url: url, 
            data: null, 
            method: 'GET', 
            cb: cb, 
            scope: scope
        });
    },
    
    post: function (url, data, cb, scope) {
        this.sendRequest({
            url: url, 
            data: data, 
            method: 'POST', 
            cb: cb, 
            scope: scope
        });
    },
    
    put: function (url, data, cb, scope) {
        this.sendRequest({
            url: url, 
            data: null, 
            method: 'PUT', 
            cb: cb, 
            scope: scope
        });
    },
    
    del: function (url, cb, scope) {
        this.sendRequest({
            url: url, 
            data: null, 
            method: 'DELETE', 
            cb: cb, 
            scope: scope
        });
    },
    
    suspendRequests: function () {
        this.suspended = true;
    },
    
    resumeRequests: function () {
        this.suspended = false;
        this.dispatchQueuedRequests();
    },
    
    queueRequest: function (req) {
        if (this.suspended) {
            this.requestQueue.push(req);
        } else {
            this.sendRequest(req);
        }
    },
    
    dispatchQueuedRequests: function () {
        var req = null;
        if (!this.suspended) {
            while (req = this.requestQueue.shift()) {
                this.sender(req);
            }
        }
    }
});


ns.Resource = extend(Observable, {
    constructor: function () {
        this.p_link = null;
        this.p_stubLoaded = false;
        this.p_fullLoaded = false;
        this.p_busy = false;
        this.p_service = this.constructor.service;
        this.p_lastResponse = null;
        this.p_properties = [];
        
        this.addEvents('populated');
    },
    
    // Add a new property
    prop: function (name, cap, type, stub) {
        var property = ns.CreateProperty(name, cap, type, stub, this);
        this[cap] = property;
        this.p_properties.push(property);
        
        return this;
    },
    
    // Clear out all buffer values
    discard: function () {
        forEach(this.p_properties, function (property) {
            property.buffer(null);
        });
        
        return this;
    },
    
    // Move the current values to the previous values 
    // and move the buffer values to the current values
    commit: function () {
        forEach(this.p_properties, function (property) {
            var currentVal = property();
            var bufferVal = property.buffer.get();
            
            property.previous(currentVal);
            
            if (bufferVal) {
                property(bufferVal);
                proptery.buffer(null);
            }
        });
        
        return this;
    },
    
    // Move the previous values to the current values 
    // and move the current values to the buffer values
    rollback: function () {
        forEach(this.p_properties, function (property) {
            var previousVal = property.previous();
            var currentVal = property();
            
            property.buffer(currentVal);
            property(previousVal);
            property.previous(null);
        });
        
        return this;
    },
    
    load: function (callback, scope) {
        if (!this.p_link) {
            throw new Error("Resource.load: Can't load object without a link to the data.");
        }
        
        log("Resource.load: Loading data from '{0}'.", this.p_link);
        
        if (this.p_fullLoaded) {
            log("Resource.load: Already loaded.");
            
            queue(function () {
                if (callback) {
                    callback.call(scope, this, this.p_lastResponse);
                }
            }, this);
            return;
        }
        
        this.p_busy = true;
        
        this.p_service.get(this.p_link, this.populator(callback, scope), this);
        
        return this;
    },
    
    reload: function (callback, scope) {
        if (!this.p_link) {
            throw new Error("Resource.reload: Can't reload object without a link to the data.");
        }
        
        log("Resource.reload: Reloading data from '{0}'.", this.p_link);
        
        this.p_busy = true;
        
        this.p_service.get(this.p_link, this.populator(callback, scope), this);
        
        return this;
    },
    
    save: function (callback, scope) {
        var data = this.constructor.exposeData(this);
        var url = this.p_link || this.p_service.path + this.constructor.path + '/';
        
        if (this.p_link) {
            log("Resource.save: Saving data to '{0}'.", url);
            
            var cached = this.constructor.getCached(url);
            
            if (!cached) {
                this.constructor.setCached(this);
                cached = this;
            }
            
            // This object is not the same object that
            // is currently cached.  We won't allow 
            // the cached object to be overwritten.
            if (cached !== this) {
                throw new Error("Resource.save: Cache mismatch.");
            }
            
            this.p_busy = true;
            
            this.p_service.post(url, data, this.populator(callback, scope), this);
            
        } else {
            log("Resource.save: Creating object at '{0}'.", url);
            
            this.p_busy = true;
            
            this.p_service.post(url, data, this.populator(callback, scope), this);
        }
        
        return this;
    },
    
    remove: function (callback, scope) {
        if (!this.p_link) {
            throw new Error("Resource.remove: Can't remove object without a link to the data.");
        }
        
        log("Resource.remove: Removing object at '{0}'.", this.p_link);
        
        this.p_busy = true;
        
        this.p_service.del(this.p_link, this.populator(callback, scope), this);
        
        return this;
    },
    
    // Populator generator - bind populator to callback and scope
    populator: function (callback, scope) {
        return function (data, response) {
            this.p_busy = false;
            
            this.p_lastResponse = response;
            
            if (data) {
                log("Resource.populator: Populating data.");
                this.populate(data);
                this.fireEvent('populated', this);
            }
            
            queue(function () {
                if (callback) {
                    callback.call(scope, data ? this : null, response);
                }
            }, this);
        }
    },
    
    setLink: function (link) {
        this.p_link = link;
    },
    
    getLink: function () {
        return this.p_link;
    },
    
    isLinkLoaded: function () {
        return this.p_link;
    },
    
    setStubLoaded: function (b) {
        this.p_stubLoaded = b;
    },
    
    isStubLoaded: function () {
        return this.p_stubLoaded;
    },
    
    setLoaded: function (b) {
        this.p_fullLoaded = b;
    },
    
    isLoaded: function () {
        return this.p_fullLoaded;
    },
    
    setBusy: function (b) {
        this.p_busy = b;
    },
    
    isBusy: function () {
        return this.p_busy;
    },
    
    populate: function (data) {
        this.constructor.populateData(this, data);
        return this;
    },
    
    expose: function () {
        return this.constructor.exposeData(this);
    }
});

// Add class methods to a Resource class to make it into a Resource Factory
ns.CreateResourceFactory = function (Resource, path, service, id) {
    Resource.service = service;
    Resource.path = path;
    Resource.idField = id;
    Resource.cache = {};
    
    Resource.getCached = function (url) {
        if (!url) {
            return null;
        }
        
        // First check to see if this class has an ID field
        // defined.  If it doesn't, then there's no way that
        // the passed url can be unique.
        if (!Resource.idField) {
            return null;
        }
        
        return Resource.cache[url];
    };
    
    Resource.setCached = function (resource, url) {
        var url = resource.getLink() || url;
        
        log("ResourceFactory.setCached: Setting object in cache for '{0}'", url);
        
        if (url) {
            Resource.cache[url] = resource;
        }
    };
    
    Resource.list = function (callback, scope) {
        var url = Resource.service.path + Resource.path + "/";
        
        log("ResourceFactory.list: Loading all objects");
        
        Resource.service.get(url, function (data) {
            var type = ns.MakeList(ns.MakeStub(ns.ResourceType(Resource)));
            var list = type.populate(data);
            if (callback) {
                callback.call(scope, list);
            }
        });
    };
    
    Resource.load = function (id, callback, scope) {
        log("ResourceFactory.load: Loading id '{0}'", id);
        
        var url = Resource.service.path + Resource.path + "/" + id;
        
        Resource.loadFromUrl(url, callback, scope);
    };
    
    Resource.loadFromUrl: function (url, callback, scope) {
        var resource = Resource.getCached(url);
        
        if (!resource) {
            log("ResourceFactory.loadFromUrl: Loading object data from '{0}'", url);
            resource = new Resource();
            resource.setLink(url);
            Resource.setCached(resource);
            
        } else {
            log('ResourceFactory.loadFromUrl: Cached object found');
        }
        
        resource.load(callback, scope);
    };
    
    Resource.populateLink = function (resource, data) {
        log("ResourceFactory.populateLink: Populating link data.");
        resource.setLink(data);
        return resource;
    };
    
    Resource.exposeLink = function (resource) {
        log("ResourceFactory.exposeLink: Exposing link data.");
        return resource.getLink();
    };
    
    Resource.populateStub = function (resource, data) {
        log("ResourceFactory.populateStub: Populating stub data.");
        return Resource.populateData(resource, data, true);
    };
    
    Resource.exposeStub = function (resource) {
        log("ResourceFactory.exposeStub: Exposing stub data.");
        return Resource.exposeData(resource, true);
    };
    
    Resource.populateData = function (resource, data, isStub) {
        log("ResourceFactory.populateData: Populating object.");
        
        resource.setLink(d.link);
        resource.setStubLoaded(true);
        
        if (!isStub) {
            resource.setLoaded(true);
        }
        
        forEach(resource.p_properties, function (property) {
            property.populate(d[property.name]);
        });
        
        return resource;
    };
    
    Resource.exposeData = function (resource, isStub) {
        log("ResourceFactory.exposeData: Exposing data.");
        
        var data = {link: resource.getLink()};
        
        forEach(resource.p_properties, function (property) {
            if (!isStub || (isStub && property.isStub)) {
                data[property.name] = property.expose();
            }
        }
        
        return data;
    };
};


// Create a property
ns.CreateProperty = function (name, cap, type, stub, resource) {
    // This is the new way...
    // name()
    // name(val)
    // name.type()
    
    var previousValue = null;
    var currentValue = null;
    var bufferValue = null;
    
    var currentUpdateEvent = name + "_updated";
    var bufferUpdateEvent = name + "_buffer_updated";
    
    // Create the primary function
    var property = function (val) {
        if (val === undefined) {
            return currentValue;
        } else {
            currentValue = val;
            resource.fireEvent(currentUpdateEvent, val);
            return resource;
        }
    };
    
    // Add on the names
    property.name = name;
    property.cap = cap;
    
    // Add on the type
    property.type = type || function () {return null;};
    
    // Add on the stub value
    property.isStub = stub;
    
    // Add on the buffer value
    property.buffer = function (val) {
        if (val === undefined) {
            return bufferValue || currentValue;
        } else {
            bufferValue = val;
            resource.fireEvent(bufferUpdateEvent, val);
            return resource;
        }
    };
    
    property.buffer.get = function () {
        return bufferValue;
    };
    
    // Add on the previous value
    property.previous = function () {
        if (val === undefined) {
            return previousValue;
        } else {
            previousValue = val;
            return resource;
        }
    };
    
    // Add on the serialization methods
    property.expose = function () {
        if (type) {
            try {
                return type().expose(currentValue);
            } catch (e) {
                return null;
            }
        } else {
            return currentValue;
        }
    };
    
    property.populate = function (val) {
        if (val !== null && val !== undefined) {
            property(type ? type().populate(val) : val);
        }
    };
    
    // Add on the add/remove listeners
    property.addListener = function (listener, scope) {
        resource.on(currentUpdateEvent, listener, scope);
    };

    property.removeListener = function (listener, scope) {
        resource.un(currentUpdateEvent, listener, scope);
    };
    
    property.buffer.addListener = function (listener, scope) {
        resource.on(bufferUpdateEvent, listener, scope);
    };
    
    property.buffer.removeListener = function (listener, scope) {
        resource.un(bufferUpdateEvent, listener, scope);
    };
    
    // Add the events to the resource
    resource.addEvents(currentUpdateEvent, bufferUpdateEvent);
    
    // This is still legacy...
    // getName()
    // setName(val)
    // getNameType()
    var capName = cap.charAt(0).toUpperCase() + cap.slice(1);
    resource['set' + capName] = property;
    resource['get' + capName] = property;
    resource['get' + capName + 'Type'] = property.type;
    
    return property;
};


ns.String = ns.Number = ns.Boolean = 'Basic';

ns.ResourceType = function (Resource) {
    if (typeof Resource !== 'function') {
        return Resource;
    }
    
    return {
        resource: Resource,
        
        getLink: function (data) {
            return data && data.link || null;
        },
        
        populate: function (data) {
            if (!data) {
                return null;
            }
            
            // Peek at the data link
            var link = this.getLink(data);
            
            var resource = this.resource.getCached(link) || new this.resource();
            
            if (link) {
                resource.setLink(link);
                this.resource.setCached(resource);
            }
            
            resource.populate = this.popMethod || resource.populate;
            resource.expose = this.expMethod || resource.expose;
            
            return resource.populate(data);
        },
        
        expose: function (obj) {
            return this.expMethod ? this.expMethod.call(obj) : obj.expose();
        },
        
        create: function () {
            var resource = new this.resource();
            
            resource.populate = this.popMethed || resource.populate;
            resource.expose = this.expMethod || resource.expose;
            
            return resource;
        }
    };
};


ns.MakeLink = function (type) {
    if (type && !type.resource) {
        return type;
    }
    
    type.popMethod = function (data) {
        this.constructor.populateLink(this, data);
        return this;
    };
    
    type.expMethod = function () {
        return this.constructor.exposeLink(this);
    };
    
    type.getLink = function (data) {
        return data;
    };
    
    return type;
};


ns.MakeStub = function (type) {
    if (type && !type.resource) {
        return type;
    }
    
    type.popMethod = function (data) {
        this.constructor.populateStub(this, data);
        return this;
    };
    
    type.expMethod = function () {
        return this.constructor.exposeStub(this);
    };
    
    return type;
};


ns.MakeList = function (type) {
    return {
        getLink: function (data) {
            return data.link;
        },
        
        populate: function (data) {
            var list = this.create();
            return list.populate(data);
        },
        
        expose: function (list) {
            return this.expMethod ? this.expMethod.call(list) : list.expose();
        },
        
        create: function () {
            var list = new ns.List(type);
            
            if (type.resource) {
                list.p_service = type.resource.service;
            }
            
            if (this.popMethod) {
                list.populate = this.popMethod;
            }
            
            if (this.expMethod) {
                list.expose = this.expMethod;
            }
            
            return list;
        }
    };
};


// Resource class to be used for all lists.
ns.List = extend(ns.Resource, {
    constructor: function (type) {
        ns.Resource.call(this);
        
        this.items = [];
        this.proto = type;
        
        this.addEvents('item_added', 'item_removed');
    },
    
    addItem: function (items) {
        if (!items) {
            return this;
        }
        
        if (!(items instanceof Array)) {
            items = [items];
        }

        log("List.addItem: Adding {0} items", items.length);
        
        var collection = this.items;
        var addedItems = [];
        
        forEach(items, function (item) {
            if (collection.indexOf(item) < 0) {
                collection.push(item);
                addedItems.push(item);
            }
        });
        
        if (addedItems.length > 0) {
            log("List.addItem: {0} new items added", addedItems.length);
            this.fireEvent('item_added', addedItems);
        }
        
        return this;
    },
    
    removeItem: function (items) {
        if (!items) {
            return false;
        }
        
        if (!(items instanceof Array)) {
            items = [items];
        }
        
        log("List.removeItem: Removing {0} items", items.length);
        
        var collection = this.items;
        var removedItems = [];
        var i, l = items.length, item;
        
        forEach(items, function (item) {
            if (collection.indexOf(item) > -1) {
                collection.remove(item);
                removedItems.push(item);
            }
        });
        
        if (removedItems.length > 0) {
            log("List.removeItem: {0} items removed", addedItems.length);
            this.fireEvent('item_removed', removedItems);
        }
        
        return this;
    },
    
    clear: function () {
        return this.removeItem(this.items.slice(0));
    },
    
    getItems: function () {
        return this.items;
    },
    
    getItemCount: function () {
        return this.items.length;
    }
});

ns.CreateResourceFactory(ns.List, '');

ns.List.populateData = function (list, data) {
    log("List.populateData: Populating data.");
    list.setStubLoaded(true);
    list.setLoaded(true);
    
    if (!(data instanceof Array)) {
        return list;
    }
    
    var oldItems = list.items;
    var newItems = [];
    var addItems = [];
    var removeItems = [];
    
    // We want to compare the result set that we get
    // with the items we currently have to determine
    // exactly which items have changed.
    
    forEach(data, function (idata) {
        var item = !list.proto.resource ? idata : list.proto.populate(idata);
        newItems.push(item);
        
        if (oldItems.indexOf(item) === -1) {
            addItems.push(item);
        }
    });
    
    forEach(oldItems, function (item) {
        if (newItems.indexOf(item) === -1) {
            removeItems.push(item);
        }
    });
    
    list.removeItem(removeItems);
    list.addItem(addItems);
    
    return list;
};

ns.List.exposeData = function (list) {
    return !list.proto.resource ? list.items : list.items.map(function (item) {
        return item.expose();
    });
};

})();